Skip to content

Avoid per-connect lock on shared Bootstrap options map (#2218)#2219

Merged
hyperxpro merged 4 commits into
AsyncHttpClient:mainfrom
maygemdev:bootstrap-lock-fix-issue-2218
Jul 18, 2026
Merged

Avoid per-connect lock on shared Bootstrap options map (#2218)#2219
hyperxpro merged 4 commits into
AsyncHttpClient:mainfrom
maygemdev:bootstrap-lock-fix-issue-2218

Conversation

@pavel-ptashyts

@pavel-ptashyts pavel-ptashyts commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #2218.

AsyncHttpClient reuses a single Bootstrap instance for every outbound
connection. On each connect, Netty's AbstractBootstrap.newOptionsArray()
copies the shared options map under synchronized (options):

synchronized (options) {
    return new ArrayList<Map.Entry<ChannelOption<?>, Object>>(options.entrySet())
            .toArray(EMPTY_OPTION_ARRAY);
}

Because the same Bootstrap (and its options map) backs every connection,
this is a single global monitor that all connection attempts contend on.
Under high connection-establishment rates — many concurrent outbound
requests, short-lived connections with low reuse, or no effective per-host
connection cap — threads queue on this lock and a convoy forms.

Fix

Stop configuring options on the Bootstrap. Instead:

  • Resolve the configured ChannelOptions once at construction into a
    fixed array (buildChannelOptions). Conditional options (connect timeout,
    SO_LINGER, send/receive buffers) and all config getters are evaluated a
    single time rather than per connection.
  • Apply that pre-resolved array to each channel from AHC's existing channel
    initializer via the public Channel.config().setOption(...) API
    (applyChannelOptions), leaving the Bootstrap options map empty.

This removes the global lock from the connect path entirely while preserving
identical behavior:

  • Same option values and ordering — user-supplied options are still
    applied last so they override defaults, matching the previous Bootstrap
    order.
  • Same timing — ChannelInitializer.initChannel runs during channel
    registration (handlerAdded, channel already registered), which is before
    the connect listener fires, so options are set before the channel connects.
    The connect-timeout option is honored because Netty reads it at connect time.
  • The SOCKS proxy path is covered automatically, since it installs the HTTP
    channel initializer into its pipeline.

Scope is limited to ChannelManager.

Benefits

  • Eliminates the lock convoy on the connect path.
  • Avoids re-reading config and rebuilding the options array per connection
    (fewer per-connect allocations).
  • No behavioral or configuration changes for users.

…nt#2218)

AHC reuses a single Bootstrap for every outbound connection. Netty's
AbstractBootstrap.newOptionsArray() copies the shared options map under
"synchronized (options)" on every connect, serializing all connection
attempts on one monitor. Under high connection-establishment rates this
becomes a lock convoy.

Resolve the configured ChannelOptions once at construction into a fixed
array and apply them to each channel from the existing channel
initializer via Channel.config().setOption(...), leaving the Bootstrap
options map empty. This removes the global lock from the connect path
and avoids re-reading config per connection, while preserving identical
option values, ordering, and timing (options are applied during channel
registration, before connect).
…nt#2218)

AHC reuses a single Bootstrap for every outbound connection. Netty's
AbstractBootstrap.newOptionsArray() copies the shared options map under
"synchronized (options)" on every connect, serializing all connection
attempts on one monitor. Under high connection-establishment rates this
becomes a lock convoy.

Resolve the configured ChannelOptions once at construction into a fixed
array and apply them to each channel from the existing channel
initializer via Channel.config().setOption(...), leaving the Bootstrap
options map empty. This removes the global lock from the connect path
and avoids re-reading config per connection, while preserving identical
option values, ordering, and timing (options are applied during channel
registration, before connect).
@pavel-ptashyts

Copy link
Copy Markdown
Contributor Author

Hi @hyperxpro could you check this PR. Thanks in advance

@hyperxpro

Copy link
Copy Markdown
Member

I will have a look soon along with your other PRs.

@hyperxpro
hyperxpro merged commit 5289f88 into AsyncHttpClient:main Jul 18, 2026
13 checks passed
hyperxpro added a commit that referenced this pull request Jul 18, 2026
Motivation

After #2219, channel options are applied per-channel in
ChannelManager.applyChannelOptions via Channel.config().setOption(...)
instead of Bootstrap#option. The direct call dropped the error handling
that Netty's AbstractBootstrap#setChannelOption provided: an unknown
option key is now silently ignored (previously logged Unknown channel
option), and an option whose setOption throws now propagates out of
initChannel as a generic channel-init failure that closes the channel
with a masked cause, instead of being logged with the offending option
and value.

Modification

In applyChannelOptions, mirror Netty's
AbstractBootstrap#setChannelOption: warn and skip when setOption returns
false (unknown option), and warn (naming the option and value) then
rethrow on failure, preserving Netty's close-on-failure semantics so a
channel never connects with a partially-applied configuration.

Result

Channel-option error handling and diagnostics match the pre-#2219
Bootstrap behavior again. Happy-path behavior is unchanged, and only
user-supplied config.getChannelOptions() can reach these branches since
the built-in defaults are guarded to valid ranges.
@pavel-ptashyts
pavel-ptashyts deleted the bootstrap-lock-fix-issue-2218 branch July 18, 2026 11:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AHC: per-connect lock contention on the shared Bootstrap options map

2 participants